package com.sec.android.app.myfiles.module.local.file;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteFullException;
import android.net.Uri;
import android.os.SystemClock;
import android.provider.MediaStore;

import com.sec.android.app.myfiles.info.AppConstants;
import com.sec.android.app.myfiles.info.FileType;
import com.sec.android.app.myfiles.info.MediaFile;
import com.sec.android.app.myfiles.log.Log;
import com.sec.android.app.myfiles.module.ModuleLoader;
import com.sec.android.app.myfiles.module.abstraction.AbsProviderImp;
import com.sec.android.app.myfiles.module.abstraction.FileRecord;
import com.sec.android.app.myfiles.module.abstraction.FileRecord.StorageType;
import com.sec.android.app.myfiles.operation.FileOperator;
import com.sec.android.app.myfiles.provider.DbTableInfo;
import com.sec.android.app.myfiles.provider.DbTableInfo.COLUMN_ID;
import com.sec.android.app.myfiles.util.FileUtils;
import com.sec.android.app.myfiles.util.SemFwWrapper;
import com.sec.android.app.myfiles.util.StorageMonitor;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class LocalFileProviderImp extends AbsProviderImp {

    private static int START_COUNT = 1024;
    static final int MAX_LOAD_COUNT = 1024;

    private static final String COLUMN_NAME_SEF_FILE_TYPE = DbTableInfo.COLUMN_NAME_SEF_FILE_TYPE;
    private static final String COLUMN_NAME_GEAR360_VIDEO = DbTableInfo.COLUMN_NAME_GEAR360_VIDEO;
    private static final int GEAR360_IMAGE_SEF_FILE_TYPE = AppConstants.SEF_FILE_TYPE_GEAR_360_IMAGE;

    private HashMap<String, Long> mFolderLastModifiedTime = new HashMap<>();
    private HashMap<String, Set<String>> mGear360ContentsMap = new HashMap<>();

    private static final int KEEP_ALIVE_TIME = 10;
    private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS;

    private final String mediaIdCol;
    private final String pathCol;
    private final String nameCol;
    private final String sizeCol;
    private final String dateCol;
    private final String isHiddenCol;
    private final String fileTypeCol;
    private final String extCol;
    private final String isDirCol;
    private final String itemCntCol;
    private final String itemCntHiddenCol;
    private final String isGear360ContentsCol;

    private Uri mFolderTreeUri = null;
    private boolean mIsLoading = false;

    private final BlockingQueue<Runnable> mLoadQueue = new LinkedBlockingQueue<>();
    private final ThreadPoolExecutor mLoadThreadPool = new ThreadPoolExecutor(
            AppConstants.NUMBER_OF_THREADS, AppConstants.NUMBER_OF_THREADS,
            AppConstants.IDLE_THREAD_KEEP_ALIVE_TIME, AppConstants.IDLE_THREAD_KEEP_ALIVE_TIME_UNIT,
            mLoadQueue);

    public LocalFileProviderImp(Context context) {
        super(context);

        mediaIdCol = mDbTableInfo.getColumnName(COLUMN_ID.MEDIA_ID);
        pathCol = mDbTableInfo.getColumnName(COLUMN_ID.PATH);
        nameCol = mDbTableInfo.getColumnName(COLUMN_ID.NAME);
        sizeCol = mDbTableInfo.getColumnName(COLUMN_ID.SIZE);
        dateCol = mDbTableInfo.getColumnName(COLUMN_ID.DATE);
        isHiddenCol = mDbTableInfo.getColumnName(COLUMN_ID.IS_HIDDEN);
        fileTypeCol = mDbTableInfo.getColumnName(COLUMN_ID.FILE_TYPE);
        extCol = mDbTableInfo.getColumnName(COLUMN_ID.EXT);
        isDirCol = mDbTableInfo.getColumnName(COLUMN_ID.IS_DIRECTORY);
        itemCntCol = mDbTableInfo.getColumnName(COLUMN_ID.ITEM_COUNT);
        itemCntHiddenCol = mDbTableInfo.getColumnName(COLUMN_ID.ITEM_COUNT_HIDDEN);
        isGear360ContentsCol = mDbTableInfo.getColumnName(COLUMN_ID.IS_360_CONTENTS);

        DbTableInfo folderTreeTableInfo = ModuleLoader.getInstance().getTableInfo(StorageType.FolderTree);
        if (folderTreeTableInfo != null) {
            mFolderTreeUri = Uri.parse(folderTreeTableInfo.getUri());
        }
    }

    @Override
    protected void updateTable(SQLiteDatabase db, int oldVersion, int newVersion) {
        super.updateTable(db, oldVersion, newVersion);
        // Existing indexes are cleared when the table is deleted.
        String createIndexSql = "CREATE INDEX IF NOT EXISTS IDX_" + getTableName() + "_PATH_NAME ON " + getTableName() + " ( " +
                pathCol + " ASC, " + nameCol + " ASC);";
        try {
            db.execSQL(createIndexSql);
        } catch (SQLiteException e) {
            Log.e(this, "SQLiteException:" + e.toString());
        }
    }

    @Override
    protected StorageType getStorage() {
        return StorageType.Local;
    }

    @Override
    protected boolean needRefresh(Uri uri, FileRecord curRecord) {
        boolean bRet = false;
        String curPath = curRecord.getFullPath();

        if (curPath != null) {
            Long lastModified = mFolderLastModifiedTime.get(curPath);
            File curFile = SemFwWrapper.file(curPath);
            long curModified = curFile.lastModified();
            /*P160822-01231 EncryptedSdCard always need to refresh*/
            boolean isEncryptedSdCard = StorageMonitor.isMountedSdCardPath(curPath) && StorageMonitor.isSdCardEncrypted();
            boolean isRemovableStorageRoot = StorageMonitor.isRemovableStorageRoot(curPath);

            Log.d(this, "needRefresh - " + lastModified + "/" + curModified);

            if (lastModified == null || lastModified != curModified || isEncryptedSdCard || isRemovableStorageRoot) {
                Log.d(this, "needRefresh - " + curPath);
                bRet = true;
                if (curFile.exists()) {
                    mFolderLastModifiedTime.put(curPath, curModified);
                } else {
                    Log.d(this, curPath + " not exist");
                    mFolderLastModifiedTime.remove(curPath);
                }
            }
        }

        return bRet;
    }

    @Override
    protected void _refreshDB(Uri uri, FileRecord curRecord) {
        if (!(FileOperator.isWorking() && FileOperator.getFileOperation() == FileOperator.Operation.MOVE)) {
            File curFile = SemFwWrapper.file(curRecord.getFullPath());
            File[] filesList = curFile.listFiles();

            try {
                deleteLocalFileProvider(curRecord);
                if (filesList != null) {
                    findGear360Contents(curRecord);
                    enqueueLoading(uri, curRecord, filesList);
                } else {
                    mFolderLastModifiedTime.remove(curRecord.getFullPath());
                }
            } catch (SQLiteFullException e) {
                Log.e(this, "SQLiteFullException:" + e.toString());
            } catch (Exception e) {
                Log.e(this, "Exception:" + e.toString());
            }
        }
    }

    private int loadingFileSize = 0;

    private void findGear360Contents(FileRecord curRecord) {
        String fullPath = curRecord.getFullPath();
        Cursor c = mContext.getContentResolver().query(FileUtils.MEDIA_PROVIDER_URI,
                new String[]{MediaStore.Files.FileColumns.DATA},
                "(" + COLUMN_NAME_GEAR360_VIDEO + "=1 OR " + COLUMN_NAME_SEF_FILE_TYPE + "=" + GEAR360_IMAGE_SEF_FILE_TYPE + ") AND " +
                        MediaStore.Files.FileColumns.DATA + " LIKE ? AND " + MediaStore.Files.FileColumns.DATA + " NOT LIKE ?",
                new String[]{fullPath + "%", fullPath + "/%/%"},
                null);
        if (c != null) {
            Set<String> set = mGear360ContentsMap.get(fullPath);
            if (set == null) {
                set = new HashSet<>();
                mGear360ContentsMap.put(fullPath, set);
            } else {
                set.clear();
            }
            if (c.moveToFirst()) {
                do {
                    set.add(c.getString(0));
                } while (c.moveToNext());
            }
            c.close();
        }
    }

    private void enqueueLoading(final Uri uri, final FileRecord curRecord, final File[] files) {
        int index = 0;
        int numberOfFiles = files.length;
        Log.d(this, "doLoading : " + curRecord.getName() + " " + index + "/" + numberOfFiles);

        loadingFileSize += numberOfFiles;

        mIsLoading = true;
        if (!load(uri, files, index, START_COUNT)) {
            _clearCache(curRecord);
            return;
        }
        index += START_COUNT;

        while (index < numberOfFiles) {
            final int currentIndex = index;
            Log.d(this, "doLoading : " + curRecord.getName() + " " + index + "/" + numberOfFiles);
            mLoadThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    if (!load(uri, files, currentIndex, MAX_LOAD_COUNT)) {
                        _clearCache(curRecord);
                        return;
                    }
                }
            });
            index += MAX_LOAD_COUNT;
        }
    }

    private void deleteLocalFileProvider(FileRecord curRecord) {
        String selection = mDbTableInfo.getColumnName(COLUMN_ID.PATH) + "=?";
        String[] selectionArgs = new String[]{curRecord.getFullPath()};

        try {
            mDB.delete(getTableName(), selection, selectionArgs);
        } catch (SQLiteFullException e) {
            Log.e(this, "SQLiteFullException:" + e.toString());
        }
    }

    private boolean load(Uri uri, File[] files, int startIndex, int limit) {
        long start = SystemClock.elapsedRealtime();
        Log.d(this, "doLoading is undergoing with DbTable : " + getTableName());
        if (!mIsLoading) {
            return false;
        }
        List<ContentValues> values = getFileContentValueFromFile(files, startIndex, limit);
        if (values != null) {
            int size = values.size();
            if (size > 0) {
                ContentValues[] valueArr = values.toArray(new ContentValues[size]);
                bulkInsert(uri, valueArr);
                Log.d(this, "Bulk insert cnt = " + valueArr.length);
            }

            loadingFileSize -= size;
            if (loadingFileSize == 0) {
                notifyChange(uri);
            }
        } else {
            return false;
        }

        Log.d(this, "doLoad time=" + (SystemClock.elapsedRealtime() - start) + "(" + Thread.currentThread().getId() + ")");
        return true;
    }

    private List<ContentValues> getFileContentValueFromFile(final File[] files, int startIndex, int limit) {
        List<ContentValues> result = new ArrayList<>();
        List<Future<ContentValues>> futures = new ArrayList<>();
        int availableProcessors = Runtime.getRuntime().availableProcessors() / 2;
        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(availableProcessors);
        for (int index = startIndex; index < files.length && index < startIndex + limit; index++) {
            if (!mIsLoading) {
                executor.shutdownNow();
                return null;
            }
            futures.add(executor.submit(new FileContentValueCallable(this, files[index])));
        }

        for (Future<ContentValues> future : futures) {
            try {
                if (!mIsLoading) {
                    executor.shutdownNow();
                    return null;
                }
                ContentValues value = future.get(KEEP_ALIVE_TIME, TIME_UNIT);
                if (value != null) {
                    result.add(value);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                future.cancel(true);
                Log.e(this, "getFileContentValueFromFile() ] TimedOutException e = " + e.getMessage());
            }
        }
        executor.shutdown();
        return result;
    }


    private ContentValues getFileContentValue(File file) {
        ContentValues ret = null;

        if (!isSkipFile(file)) {
            ret = new ContentValues();

            String fullPath = file.getAbsolutePath();

            ret.put(mediaIdCol, -1);
            ret.put(pathCol, file.getParent());
            ret.put(nameCol, file.getName());
            ret.put(sizeCol, file.length());
            ret.put(dateCol, file.lastModified());
            ret.put(isHiddenCol, file.isHidden() ? 1 : 0);

            if (file.isDirectory()) {
                ret.put(isHiddenCol, FileUtils.isUnderHiddenFolder(fullPath) ? 1 : 0);
                ret.put(fileTypeCol, FileType.FOLDER);
                ret.put(itemCntCol, 0);
                ret.put(itemCntHiddenCol, 0);
                ret.put(isDirCol, 1);
            } else {
                ret.put(isHiddenCol, file.isHidden() ? 1 : 0);
                ret.put(fileTypeCol, MediaFile.getFileType(fullPath, mContext));
                ret.put(extCol, MediaFile.getExtensionAsUpperCase(file.getName()));
                ret.put(isDirCol, 0);

                Set<String> gear360ContentsSet = mGear360ContentsMap.get(file.getParent());
                ret.put(isGear360ContentsCol, gear360ContentsSet != null ?
                        gear360ContentsSet.contains(fullPath) : false);
            }
        } else {
            loadingFileSize--;
        }

        return ret;
    }

    private boolean isSkipFile(File f) {
        boolean bRet = false;

        if (!f.canRead()) {
            bRet = true;
        } else {
            String path = f.getAbsolutePath();
            if (FileUtils.isSystemFolder(path)) {
                bRet = true;
            }
        }
        return bRet;
    }

    @Override
    protected void _clearCache(FileRecord record) {
        if (record != null) {
            Log.d(this, "_clearCache : " + record.getFullPath());
            mFolderLastModifiedTime.remove(record.getFullPath());
        } else {
            Log.d(this, "_clearCache all");
            mFolderLastModifiedTime.clear();
        }
    }

    protected void notifyChange(Uri uri) {
        super.notifyChange(uri);

        if (mFolderTreeUri != null) {
            super.notifyChange(mFolderTreeUri);
        }
    }

    @Override
    protected boolean needClearDB() {
        return true;
    }

    @Override
    protected void _stopRefreshDb() {
        mIsLoading = false;
    }

    private static class FileContentValueCallable implements Callable<ContentValues> {

        private LocalFileProviderImp mProvider;
        private File mFile;

        public FileContentValueCallable(LocalFileProviderImp provider, File file) {
            mProvider = provider;
            mFile = file;
        }

        @Override
        public ContentValues call() throws Exception {
            return mProvider.getFileContentValue(mFile);
        }
    }
}